// *********************************************************
//      Copyright (c) Kosta Gaitanis
//      All rights reserved
//
//      k.gaitanis@gmail.com
//
// *********************************************************

#define GL_GLEXT_PROTOTYPES 1

#include <QGLShader>
#include "Vector.h"
#include "Matrix.h"
#include "GLPipeline.h"


GLPipeline::GLPipeline(QObject *parent) :
    QObject(parent)
{
}

GLPipeline::~GLPipeline()
{

}

QVariant GLPipeline::getPipelineParameter(const QString &name) const
{
    Q_UNUSED(name);
    return QVariant();
}

void GLPipeline::initialize()
{

}

void GLPipeline::render()
{
    emit requestPaintItems();
}

void GLPipeline::resize(int w, int h)
{
    Q_UNUSED(w);
    Q_UNUSED(h);
}

void GLPipeline::setPipelineParameter(const QString &name, const QVariant &value)
{
    Q_UNUSED(name);
    Q_UNUSED(value);
}

//////////////////////////////////////

FBOPipeline::FBOPipeline(QObject *parent) :
    GLPipeline(parent),
    m_shaderProgram(0),
    m_finalRenderProgram(0),
    m_blurProgram(0),
    m_gamma(1.0),
    m_blurSamples(10),
    m_FBO(0),
    m_renderTexture(0),
    m_depthTexture(0),
    m_glowTexture(0),
    m_glowHBlurTexture(0),
    m_renderTextureSize(1024,768),
    m_glowTextureSize(800,600),
    m_renderGlow(true)
{

}

FBOPipeline::~FBOPipeline()
{
    glDeleteFramebuffers(1, &m_FBO);
    glDeleteTextures(1, &m_renderTexture);
    glDeleteTextures(1, &m_glowTexture);
    glDeleteTextures(1, &m_glowHBlurTexture);
    glDeleteTextures(1, &m_depthTexture);
}

QSize FBOPipeline::getGLSize() const
{
    if (!m_glSize.isValid()) {
        // GL has not been resized yet.
        return QSize(1,1);
    }
    return m_glSize;
}

QSize FBOPipeline::getRenderResolution() const
{
    if (m_renderTextureSize.isValid()) {
        return m_renderTextureSize;
    }
    // render resolution is the same as glsize
    return getGLSize();
}

QVariant FBOPipeline::getPipelineParameter(const QString &name) const
{
    if (name.toLower() == "gamma") {
        return m_gamma;
    } else if (name.toLower() == "renderglow") {
        return m_renderGlow;
    } else if (name.toLower() == "glowsize") {
        return m_glowTextureSize;
    } else if (name.toLower() == "rendersize") {
        return m_renderTextureSize;
    } else if (name.toLower() == "blursamples") {
        return m_blurSamples;
    }

    return QVariant();
}

void FBOPipeline::setGamma(double gamma)
{
    if (m_shaderProgram) {
        m_shaderProgram->bind();
        m_shaderProgram->setUniformValue("gamma", (GLfloat) gamma);
        m_gamma = gamma;
        if (int e = glGetError()) {
            qDebug() << "error while setting gamma" << e;
        }
        updateGL();
    }
}

void FBOPipeline::setRenderGlow(bool render)
{
    m_renderGlow = render;
    updateGL();
}

void FBOPipeline::setGlowTextureSize(QSize size)
{
    m_glowTextureSize = size;
    resize(m_glSize.width(), m_glSize.height());
    updateGL();
}

void FBOPipeline::setBlurSamples(int samples)
{
    if (m_blurProgram) {
        m_blurProgram->bind();
        m_blurProgram->setUniformValue("n", samples);
        if (int e = glGetError()) {
            qDebug() << "error while setting blur samples" << e;
        }
        m_shaderProgram->bind();
        updateGL();
    }
}

void FBOPipeline::setRenderSize(QSize size)
{
    m_renderTextureSize = size;
    resize(m_glSize.width(), m_glSize.height());
    updateGL();
}

void FBOPipeline::initialize()
{
    if (glGetError()) {
        qDebug() << "FBOPipeline::There was an error somewhere before initialization.";
    }

    createShaders();
    createTextures();

    // create the frame buffer
    glGenFramebuffers(1, &m_FBO);
}

void FBOPipeline::render()
{
    renderScene();

    if (m_renderGlow) {
        renderGlow();
    }

    // switch back to initial shader program to capture gamma changes from the GUI
    m_shaderProgram->bind();
}

void FBOPipeline::resize(int w, int h)
{
    m_glSize = QSize(w,h);

    // resize the rendering and glow texture
    if (m_glowTexture) {
        glBindTexture(GL_TEXTURE_2D, m_glowTexture);
        glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, getRenderResolution().width(), getRenderResolution().height(), 0,GL_RGB, GL_UNSIGNED_BYTE, NULL);
    }
    if (m_glowHBlurTexture) {
        glBindTexture(GL_TEXTURE_2D, m_glowHBlurTexture);
        glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA, m_glowTextureSize.width(), m_glowTextureSize.height(), 0,GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    }
    if (m_renderTexture) {
        glBindTexture(GL_TEXTURE_2D, m_renderTexture);
        glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA, getRenderResolution().width(), getRenderResolution().height(), 0,GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    }
    // resize The depth texture
    if (m_depthTexture) {
        glBindTexture(GL_TEXTURE_2D, m_depthTexture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, getRenderResolution().width(), getRenderResolution().height(), 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
    }
}

void FBOPipeline::setPipelineParameter(const QString &name, const QVariant &value)
{
    if (name.toLower() == "gamma") {
        setGamma(value.toFloat());
    } else if (name.toLower() == "renderglow") {
        setRenderGlow(value.toBool());
    } else if (name.toLower() == "glowsize") {
        setGlowTextureSize(value.toSize());
    } else if (name.toLower() == "rendersize") {
        setRenderSize(value.toSize());
    } else if (name.toLower() == "blursamples") {
        setBlurSamples(value.toInt());
    }
}

void FBOPipeline::paintTextureQuad(GLuint texture)
{
    // disable depth test to allow overlay painting using quads
    glDisable(GL_DEPTH_TEST);

    glBindTexture(GL_TEXTURE_2D, texture);
    glBegin(GL_QUADS);
    glVertex2d(-1,-1);
    glVertex2d(-1,1);
    glVertex2d(1,1);
    glVertex2d(1,-1);
    glEnd();
}

void FBOPipeline::createShaders()
{
    m_shaderProgram = new QGLShaderProgram(this);
    m_shaderProgram->addShaderFromSourceFile(QGLShader::Vertex, ":/shaders/vertex");
    m_shaderProgram->addShaderFromSourceFile(QGLShader::Fragment, ":/shaders/fragment");
    m_shaderProgram->link();
    m_shaderProgram->bind();
    m_shaderProgram->setUniformValue("gamma", (GLfloat) m_gamma);

    m_finalRenderProgram = new QGLShaderProgram(this);
    m_finalRenderProgram->addShaderFromSourceFile(QGLShader::Vertex, ":/shaders/passThrough-vertex");
    m_finalRenderProgram->addShaderFromSourceFile(QGLShader::Fragment, ":/shaders/passThrough-fragment");
    m_finalRenderProgram->link();

    m_blurProgram = new QGLShaderProgram(this);
    m_blurProgram->addShaderFromSourceFile(QGLShader::Vertex, ":/shaders/passThrough-vertex");
    m_blurProgram->addShaderFromSourceFile(QGLShader::Fragment, ":/shaders/blur-fragment");
    m_blurProgram->link();
    m_blurProgram->bind();
    m_blurProgram->setUniformValue("n", m_blurSamples);

    if (GLenum error = glGetError()) {
        qDebug() << "FBOPipeline::There was an error while loading one of the shaders.";
        qDebug() << error;
    }

    // make sure this is the bounded program to catch gamma changes from the GUI
    m_shaderProgram->bind();
}

void FBOPipeline::createTextures()
{
    int w = getRenderResolution().width();
    int h = getRenderResolution().height();

    // Create a 1x1 pixel white image and set it as texture 0
    // this is used to render non-textures objects and has no effect
    // on final fragment color.
    QImage white(QSize(1,1), QImage::Format_ARGB32);
    white.fill(Qt::white);
    QImage texture = QGLWidget::convertToGLFormat(white);

    // bind this texture as texture 0
    glBindTexture(GL_TEXTURE_2D, 0);
    // load the new image on the GPU
    glTexImage2D(GL_TEXTURE_2D, 0, 4, texture.width(), texture.height(), 0, GL_RGBA,
                 GL_UNSIGNED_BYTE, texture.bits());

    //////////////////////////////

    ////////////////////
    // The glow Texture
    glGenTextures(1, &m_glowTexture);
    glBindTexture(GL_TEXTURE_2D, m_glowTexture);

    glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, w, h, 0,GL_RGB, GL_UNSIGNED_BYTE, NULL);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    //////////////////////
    /// The rendering texture
    glGenTextures(1, &m_renderTexture);
    glBindTexture(GL_TEXTURE_2D, m_renderTexture);

    // Allocate memory for texture. memory is uninitialized.
    glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA, w, h, 0,GL_RGBA, GL_UNSIGNED_BYTE, NULL);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    ///////////////////////
    /// The depth Texture
    glGenTextures(1, &m_depthTexture);
    glBindTexture(GL_TEXTURE_2D, m_depthTexture);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

    // Allocate memory for texture. memory is uninitialized.
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, w, h, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
    glBindTexture(GL_TEXTURE_2D, 0);


    //////////////////////
    /// The horizontal blurred glow intermediate texture
    glGenTextures(1, &m_glowHBlurTexture);
    glBindTexture(GL_TEXTURE_2D, m_glowHBlurTexture);

    // Allocate memory for texture. memory is uninitialized.
    glTexImage2D(GL_TEXTURE_2D, 0,GL_RGBA, m_glowTextureSize.width(), m_glowTextureSize.height(), 0,GL_RGBA, GL_UNSIGNED_BYTE, NULL);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    if (int e = glGetError()) {
        qDebug() << "FBOPipeline::Error while initializing textures" << e;
    }
}

void FBOPipeline::renderScene()
{
    int w = getGLSize().width();
    int h = getGLSize().height();
    // Render using our framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    glViewport(0,0,getRenderResolution().width(),getRenderResolution().height());
    // setup FBO to render on both render and glow textures, and use a depth buffer
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_depthTexture, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_renderTexture, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, m_glowTexture, 0);
    GLenum drawBuffers1[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
    glDrawBuffers(2, drawBuffers1);

    // Always check that our framebuffer is ok
    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        qDebug() << "Problem with the FBO";
    }

    glEnable(GL_DEPTH_TEST);

    // tell all the items to paint themselves
    requestPaintItems();

    // Render to the screen
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0,0,w,h);

    glClear(GL_COLOR_BUFFER_BIT);

    // paint the rendered scene
    m_finalRenderProgram->bind();
    paintTextureQuad(m_renderTexture);
}

void FBOPipeline::renderGlow()
{
    int w = getGLSize().width();
    int h = getGLSize().height();

    // Render using our framebuffer
    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    // setup FBO to output to the glow intermediate texture. No depth texture
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, m_glowHBlurTexture, 0);

    // keep the renderTexture attached but don't render on it. Render only to glow h-blur texture
    GLenum drawBuffers2[1] = {GL_COLOR_ATTACHMENT1};
    glDrawBuffers(1, drawBuffers2);

    // Always check that our framebuffer is ok
    if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        qDebug() << "Problem with the FBO";
    }

    glClear(GL_COLOR_BUFFER_BIT);

    // paint the glow texture using the h-blur shader
    m_blurProgram->bind();
    float blurSize = 50 / (float)w;
    m_blurProgram->setUniformValue("blurSize", (GLfloat) blurSize);
    // set up for horizontal blurring
    m_blurProgram->setUniformValue("blurDirection", QSizeF(1.0, 0.0));
    // downsample the glow
    glViewport(0,0,m_glowTextureSize.width(),m_glowTextureSize.height());

    paintTextureQuad(m_glowTexture);

    // Render to the screen
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glViewport(0,0,w,h);

    // paint the h-blurred glow through the vertical blur shader
    m_blurProgram->bind();
    m_blurProgram->setUniformValue("blurSize", (GLfloat) blurSize * w / h);
    // set up blur for vertical blurring
    m_blurProgram->setUniformValue("blurDirection", QSizeF(0.0, 1.0));
    paintTextureQuad(m_glowHBlurTexture);
}


